All files / web/src/app/api/players/[id]/abacus-identity route.ts

89.13% Statements 82/92
75% Branches 12/16
100% Functions 0/0
89.13% Lines 82/92

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 931x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x     3x 3x 3x 3x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 3x 3x 3x       3x 1x 1x 12x 12x 12x 12x     12x 12x 12x 12x 1x 1x 11x 11x 12x 12x 8x 8x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 12x       12x  
/**
 * API route for the player-bound "my abacus" identity
 *
 * GET /api/players/[id]/abacus-identity
 * PUT /api/players/[id]/abacus-identity
 *
 * The identity triple (color scheme + palette + column count) the Abacus
 * Studio seeds from when this player is selected. Absent row reads as the
 * stock defaults; GET never creates (a 'view'-level teacher must not write).
 * Writes require 'start-session' access: a parent, or a teacher while the
 * student is present.
 */
 
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { db } from '@/db'
import { playerAbacusIdentity } from '@/db/schema/player-abacus-identity'
import {
  type AbacusIdentity,
  DEFAULT_ABACUS_IDENTITY,
  parseAbacusIdentity,
} from '@/lib/abacus/identity'
import { withAuth } from '@/lib/auth/withAuth'
import { canPerformAction } from '@/lib/classroom'
import { getUserId } from '@/lib/viewer'
 
export const GET = withAuth(async (_request, { params }) => {
  try {
    const { id: playerId } = (await params) as { id: string }
 
    if (!playerId) {
      return NextResponse.json({ error: 'Player ID required' }, { status: 400 })
    }
 
    const userId = await getUserId()
    const canView = await canPerformAction(userId, playerId, 'view')
    if (!canView) {
      return NextResponse.json({ error: 'Not authorized' }, { status: 403 })
    }
 
    const row = await db
      .select()
      .from(playerAbacusIdentity)
      .where(eq(playerAbacusIdentity.playerId, playerId))
      .get()
 
    const identity: AbacusIdentity = row
      ? { colorScheme: row.colorScheme, colorPalette: row.colorPalette, columns: row.columns }
      : DEFAULT_ABACUS_IDENTITY
 
    return NextResponse.json({ identity })
  } catch (error) {
    console.error('Error fetching abacus identity:', error)
    return NextResponse.json({ error: 'Failed to fetch abacus identity' }, { status: 500 })
  }
})
 
export const PUT = withAuth(async (request, { params }) => {
  try {
    const { id: playerId } = (await params) as { id: string }
 
    if (!playerId) {
      return NextResponse.json({ error: 'Player ID required' }, { status: 400 })
    }
 
    const userId = await getUserId()
    const canEdit = await canPerformAction(userId, playerId, 'start-session')
    if (!canEdit) {
      return NextResponse.json({ error: 'Not authorized' }, { status: 403 })
    }
 
    const body = (await request.json().catch(() => null)) as { identity?: unknown } | null
    const identity = parseAbacusIdentity(body?.identity)
    if (!identity) {
      return NextResponse.json({ error: 'Invalid abacus identity' }, { status: 400 })
    }
 
    const now = Date.now()
    await db
      .insert(playerAbacusIdentity)
      .values({ playerId, ...identity, updatedAt: now })
      .onConflictDoUpdate({
        target: playerAbacusIdentity.playerId,
        set: { ...identity, updatedAt: now },
      })
 
    return NextResponse.json({ identity })
  } catch (error) {
    console.error('Error saving abacus identity:', error)
    return NextResponse.json({ error: 'Failed to save abacus identity' }, { status: 500 })
  }
})